ปลดล็อกเคล็ดลับการ cleanup effect ใน React custom hook เรียนรู้วิธีป้องกัน memory leak จัดการทรัพยากร และสร้างแอป React ที่เสถียรและมีประสิทธิภาพสูงสำหรับผู้ใช้ทั่วโลก
การ Cleanup Effect ใน React Custom Hook: เชี่ยวชาญการจัดการ Lifecycle เพื่อแอปพลิเคชันที่แข็งแกร่ง
ในโลกของการพัฒนาเว็บสมัยใหม่ที่กว้างใหญ่และเชื่อมโยงถึงกัน React ได้กลายเป็นเครื่องมือสำคัญที่ช่วยให้นักพัฒนาสามารถสร้างส่วนติดต่อผู้ใช้ (UI) ที่ไดนามิกและโต้ตอบได้ หัวใจสำคัญของ functional component ใน React คือ useEffect hook ซึ่งเป็นเครื่องมืออันทรงพลังสำหรับจัดการ side effects อย่างไรก็ตาม พลังที่ยิ่งใหญ่มาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง การทำความเข้าใจวิธี cleanup effect เหล่านี้อย่างถูกต้องจึงไม่ใช่แค่แนวทางปฏิบัติที่ดีที่สุด แต่เป็นข้อกำหนดพื้นฐานสำหรับการสร้างแอปพลิเคชันที่เสถียร มีประสิทธิภาพ และเชื่อถือได้เพื่อรองรับผู้ใช้ทั่วโลก
คู่มือฉบับสมบูรณ์นี้จะเจาะลึกในประเด็นที่สำคัญของการ cleanup effect ภายใน React custom hooks เราจะสำรวจว่าทำไมการ cleanup จึงเป็นสิ่งที่ขาดไม่ได้ ตรวจสอบสถานการณ์ทั่วไปที่ต้องให้ความสนใจกับการจัดการ lifecycle อย่างพิถีพิถัน และนำเสนอตัวอย่างที่นำไปใช้ได้จริงในระดับสากลเพื่อช่วยให้คุณเชี่ยวชาญทักษะที่จำเป็นนี้ ไม่ว่าคุณจะกำลังพัฒนาแพลตฟอร์มโซเชียล เว็บไซต์อีคอมเมิร์ซ หรือแดชบอร์ดวิเคราะห์ข้อมูล หลักการที่กล่าวถึงในที่นี้มีความสำคัญอย่างยิ่งต่อการรักษาสุขภาพและความเร็วในการตอบสนองของแอปพลิเคชัน
ทำความเข้าใจ useEffect Hook ของ React และ Lifecycle ของมัน
ก่อนที่เราจะเริ่มต้นการเดินทางสู่การเป็นผู้เชี่ยวชาญด้านการ cleanup เรามาทบทวนพื้นฐานของ useEffect hook กันก่อน useEffect ซึ่งเปิดตัวมาพร้อมกับ React Hooks ช่วยให้ functional component สามารถดำเนินการ side effects ได้ ซึ่งเป็นการกระทำที่อยู่นอกเหนือจาก React component tree เพื่อโต้ตอบกับเบราว์เซอร์, เครือข่าย หรือระบบภายนอกอื่นๆ ซึ่งอาจรวมถึงการดึงข้อมูล, การเปลี่ยนแปลง DOM ด้วยตนเอง, การตั้งค่า subscriptions หรือการเริ่มต้น timers
พื้นฐานของ useEffect: เอฟเฟกต์ทำงานเมื่อใด
โดยค่าเริ่มต้น ฟังก์ชันที่ส่งเข้าไปใน useEffect จะทำงานหลังจากคอมโพเนนต์ของคุณเรนเดอร์เสร็จสิ้นทุกครั้ง ซึ่งอาจเป็นปัญหาได้หากไม่ได้รับการจัดการอย่างถูกต้อง เนื่องจาก side effects อาจทำงานโดยไม่จำเป็น ทำให้เกิดปัญหาด้านประสิทธิภาพหรือพฤติกรรมที่ผิดพลาด เพื่อควบคุมว่าเอฟเฟกต์จะทำงานซ้ำเมื่อใด useEffect รับอาร์กิวเมนต์ตัวที่สอง ซึ่งก็คือ dependency array
- หากไม่ระบุ dependency array เอฟเฟกต์จะทำงานหลังจากการเรนเดอร์ทุกครั้ง
- หากระบุเป็น array ว่าง (
[]) เอฟเฟกต์จะทำงานเพียงครั้งเดียวหลังจากการเรนเดอร์ครั้งแรก (คล้ายกับcomponentDidMount) และฟังก์ชัน cleanup จะทำงานครั้งเดียวเมื่อคอมโพเนนต์ unmount (คล้ายกับcomponentWillUnmount) - หากระบุ array ที่มี dependencies (
[dep1, dep2]) เอฟเฟกต์จะทำงานซ้ำก็ต่อเมื่อ dependencies เหล่านั้นมีการเปลี่ยนแปลงระหว่างการเรนเดอร์
พิจารณาโครงสร้างพื้นฐานนี้:
You clicked {count} times
import React, { useEffect, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// เอฟเฟกต์นี้จะทำงานหลังจากการเรนเดอร์ทุกครั้งหากไม่ได้ระบุ dependency array
// หรือเมื่อ 'count' เปลี่ยนแปลงหาก [count] เป็น dependency
document.title = `Count: ${count}`;
// ฟังก์ชันที่ return กลับไปคือกลไกการ cleanup
return () => {
// ส่วนนี้จะทำงานก่อนที่เอฟเฟกต์จะทำงานซ้ำ (หาก dependency เปลี่ยนแปลง)
// และเมื่อคอมโพเนนต์ถูก unmount
console.log('Cleanup for count effect');
};
}, [count]); // Dependency array: เอฟเฟกต์จะทำงานซ้ำเมื่อ count เปลี่ยนแปลง
return (
ส่วน "Cleanup": ทำงานเมื่อไหร่และทำไมจึงสำคัญ
กลไกการ cleanup ของ useEffect คือฟังก์ชันที่ถูก return โดย effect callback ฟังก์ชันนี้มีความสำคัญอย่างยิ่งเพราะมันช่วยให้แน่ใจว่าทรัพยากรใดๆ ที่ถูกจัดสรรหรือการดำเนินการที่เริ่มต้นโดยเอฟเฟกต์จะถูกยกเลิกหรือหยุดอย่างถูกต้องเมื่อไม่จำเป็นอีกต่อไป ฟังก์ชัน cleanup จะทำงานในสองสถานการณ์หลัก:
- ก่อนที่เอฟเฟกต์จะทำงานซ้ำ: หากเอฟเฟกต์มี dependencies และ dependencies เหล่านั้นเปลี่ยนแปลง ฟังก์ชัน cleanup จากการทำงานของเอฟเฟกต์ก่อนหน้าจะทำงานก่อนที่เอฟเฟกต์ใหม่จะทำงาน ซึ่งช่วยให้แน่ใจว่าเอฟเฟกต์ใหม่เริ่มต้นจากสภาวะที่สะอาด
- เมื่อคอมโพเนนต์ unmount: เมื่อคอมโพเนนต์ถูกลบออกจาก DOM ฟังก์ชัน cleanup จากการทำงานของเอฟเฟกต์ครั้งล่าสุดจะทำงาน ซึ่งเป็นสิ่งจำเป็นสำหรับการป้องกัน memory leak และปัญหาอื่นๆ
ทำไมการ cleanup นี้จึงมีความสำคัญอย่างยิ่งสำหรับการพัฒนาแอปพลิเคชันระดับโลก?
- การป้องกัน Memory Leaks: event listeners ที่ไม่ได้ยกเลิก, timers ที่ไม่ได้เคลียร์ หรือการเชื่อมต่อเครือข่ายที่ไม่ได้ปิด อาจยังคงอยู่ในหน่วยความจำแม้ว่าคอมโพเนนต์ที่สร้างมันขึ้นมาจะถูก unmount ไปแล้ว เมื่อเวลาผ่านไป ทรัพยากรที่ถูกลืมเหล่านี้จะสะสม ทำให้ประสิทธิภาพลดลง แอปพลิเคชันช้าลง และในที่สุดอาจทำให้แอปพลิเคชันล่ม ซึ่งเป็นประสบการณ์ที่น่าหงุดหงิดสำหรับผู้ใช้ทุกคนทั่วโลก
- การหลีกเลี่ยงพฤติกรรมที่ไม่คาดคิดและบั๊ก: หากไม่มีการ cleanup ที่เหมาะสม เอฟเฟกต์เก่าอาจยังคงทำงานกับข้อมูลที่ล้าสมัยหรือโต้ตอบกับ DOM element ที่ไม่มีอยู่จริง ทำให้เกิดข้อผิดพลาดขณะทำงาน, การอัปเดต UI ที่ไม่ถูกต้อง หรือแม้แต่ช่องโหว่ด้านความปลอดภัย ลองนึกภาพ subscription ที่ยังคงดึงข้อมูลสำหรับคอมโพเนนต์ที่ไม่ปรากฏบนหน้าจออีกต่อไป ซึ่งอาจทำให้เกิด network request หรือการอัปเดต state โดยไม่จำเป็น
- การเพิ่มประสิทธิภาพ: การปล่อยทรัพยากรอย่างรวดเร็วช่วยให้แอปพลิเคชันของคุณยังคงทำงานได้อย่างกระชับและมีประสิทธิภาพ ซึ่งมีความสำคัญอย่างยิ่งสำหรับผู้ใช้บนอุปกรณ์ที่มีประสิทธิภาพน้อยกว่าหรือมีแบนด์วิดท์เครือข่ายที่จำกัด ซึ่งเป็นสถานการณ์ทั่วไปในหลายพื้นที่ของโลก
- การรับรองความสอดคล้องของข้อมูล: การ Cleanup ช่วยรักษาสถานะที่คาดเดาได้ ตัวอย่างเช่น หากคอมโพเนนต์ดึงข้อมูลแล้วผู้ใช้ไปยังหน้าอื่น การ cleanup การดึงข้อมูลจะป้องกันไม่ให้คอมโพเนนต์พยายามประมวลผลการตอบกลับที่มาถึงหลังจากที่มันถูก unmount ไปแล้ว ซึ่งอาจนำไปสู่ข้อผิดพลาด
สถานการณ์ทั่วไปที่ต้องการ Effect Cleanup ใน Custom Hooks
Custom hooks เป็นคุณสมบัติที่ทรงพลังใน React สำหรับการแยกตรรกะที่มี state และ side effects ออกมาเป็นฟังก์ชันที่นำกลับมาใช้ใหม่ได้ เมื่อออกแบบ custom hooks การ cleanup จะกลายเป็นส่วนสำคัญของความแข็งแกร่งของมัน เรามาสำรวจสถานการณ์ที่พบบ่อยที่สุดที่การ cleanup effect เป็นสิ่งจำเป็นอย่างยิ่ง
1. Subscriptions (WebSockets, Event Emitters)
แอปพลิเคชันสมัยใหม่จำนวนมากต้องพึ่งพาข้อมูลหรือการสื่อสารแบบเรียลไทม์ WebSockets, server-sent events หรือ custom event emitters เป็นตัวอย่างที่ชัดเจน เมื่อคอมโพเนนต์สมัครรับข้อมูล (subscribe) จากสตรีมดังกล่าว สิ่งสำคัญคือต้องยกเลิกการสมัคร (unsubscribe) เมื่อคอมโพเนนต์ไม่ต้องการข้อมูลอีกต่อไป มิฉะนั้น subscription จะยังคงทำงานอยู่ ทำให้สิ้นเปลืองทรัพยากรและอาจก่อให้เกิดข้อผิดพลาด
ตัวอย่าง: useWebSocket Custom Hook
Connection status: {isConnected ? 'Online' : 'Offline'} Latest Message: {message}
import React, { useEffect, useState } from 'react';
function useWebSocket(url) {
const [message, setMessage] = useState(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
console.log('WebSocket connected');
setIsConnected(true);
};
ws.onmessage = (event) => {
console.log('Received message:', event.data);
setMessage(event.data);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setIsConnected(false);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setIsConnected(false);
};
// ฟังก์ชันสำหรับ cleanup
return () => {
if (ws.readyState === WebSocket.OPEN) {
console.log('Closing WebSocket connection');
ws.close();
}
};
}, [url]); // เชื่อมต่อใหม่หาก URL เปลี่ยนแปลง
return { message, isConnected };
}
// การใช้งานในคอมโพเนนต์:
function RealTimeDataDisplay() {
const { message, isConnected } = useWebSocket('wss://echo.websocket.events');
return (
Real-time Data Status
ใน useWebSocket hook นี้ ฟังก์ชัน cleanup ช่วยให้แน่ใจว่าหากคอมโพเนนต์ที่ใช้ hook นี้ unmount (เช่น ผู้ใช้นำทางไปยังหน้าอื่น) การเชื่อมต่อ WebSocket จะถูกปิดอย่างเรียบร้อย หากไม่มีสิ่งนี้ การเชื่อมต่อจะยังคงเปิดอยู่ ทำให้สิ้นเปลืองทรัพยากรเครือข่ายและอาจพยายามส่งข้อความไปยังคอมโพเนนต์ที่ไม่มีอยู่ใน UI อีกต่อไป
2. Event Listeners (DOM, Global Objects)
การเพิ่ม event listeners ไปยัง document, window หรือ DOM elements ที่เฉพาะเจาะจงเป็น side effect ที่พบบ่อย อย่างไรก็ตาม listeners เหล่านี้จะต้องถูกลบออกเพื่อป้องกัน memory leak และเพื่อให้แน่ใจว่า handlers จะไม่ถูกเรียกในคอมโพเนนต์ที่ถูก unmount ไปแล้ว
ตัวอย่าง: useClickOutside Custom Hook
hook นี้จะตรวจจับการคลิกนอก element ที่อ้างอิงถึง ซึ่งมีประโยชน์สำหรับเมนู dropdown, modals หรือเมนูนำทาง
This is a modal dialog.
import React, { useEffect } from 'react';
function useClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
// ไม่ต้องทำอะไรหากคลิกที่ element ของ ref หรือ element ลูกหลาน
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
// ฟังก์ชัน cleanup: ลบ event listeners ออก
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]); // ทำงานซ้ำเมื่อ ref หรือ handler เปลี่ยนแปลงเท่านั้น
}
// การใช้งานในคอมโพเนนต์:
function Modal() {
const modalRef = React.useRef();
const [isOpen, setIsOpen] = React.useState(true);
useClickOutside(modalRef, () => setIsOpen(false));
if (!isOpen) return null;
return (
Click Outside to Close
การ cleanup ในที่นี้มีความสำคัญอย่างยิ่ง หาก modal ถูกปิดและคอมโพเนนต์ unmount, listeners ของ mousedown และ touchstart จะยังคงอยู่บน document ซึ่งอาจทำให้เกิดข้อผิดพลาดหากพยายามเข้าถึง ref.current ที่ไม่มีอยู่แล้ว หรือนำไปสู่การเรียก handler ที่ไม่คาดคิด
3. Timers (setInterval, setTimeout)
Timers มักใช้สำหรับแอนิเมชัน, การนับถอยหลัง หรือการอัปเดตข้อมูลเป็นระยะๆ timers ที่ไม่ได้รับการจัดการเป็นสาเหตุคลาสสิกของ memory leak และพฤติกรรมที่ไม่คาดคิดในแอปพลิเคชัน React
ตัวอย่าง: useInterval Custom Hook
hook นี้ให้ setInterval แบบ declarative ที่จัดการการ cleanup โดยอัตโนมัติ
import React, { useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// จดจำ callback ล่าสุด
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// ตั้งค่า interval
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
// ฟังก์ชัน cleanup: เคลียร์ interval
return () => clearInterval(id);
}
}, [delay]);
}
// การใช้งานในคอมโพเนนต์:
function Counter() {
const [count, setCount] = React.useState(0);
useInterval(() => {
// โลจิกที่คุณกำหนดเองที่นี่
setCount(count + 1);
}, 1000); // อัปเดตทุก 1 วินาที
return Counter: {count}
;
}
ในที่นี้ ฟังก์ชัน cleanup clearInterval(id) มีความสำคัญอย่างยิ่ง หากคอมโพเนนต์ Counter unmount โดยไม่มีการเคลียร์ interval, callback ของ `setInterval` จะยังคงทำงานทุกวินาที และพยายามเรียก setCount ในคอมโพเนนต์ที่ถูก unmount ไปแล้ว ซึ่ง React จะแสดงคำเตือนและอาจนำไปสู่ปัญหา memory ได้
4. การดึงข้อมูลและ AbortController
แม้ว่า API request ที่สำเร็จแล้วโดยทั่วไปไม่ต้องการ 'cleanup' ในแง่ของการ 'ยกเลิก' การกระทำที่เสร็จสมบูรณ์ แต่ request ที่กำลังดำเนินอยู่สามารถทำได้ หากคอมโพเนนต์เริ่มต้นการดึงข้อมูลแล้ว unmount ก่อนที่ request จะเสร็จสิ้น promise อาจยังคง resolve หรือ reject ซึ่งอาจนำไปสู่การพยายามอัปเดต state ของคอมโพเนนต์ที่ถูก unmount ไปแล้ว AbortController เป็นกลไกที่ช่วยยกเลิก fetch request ที่ค้างอยู่
ตัวอย่าง: useDataFetch Custom Hook พร้อม AbortController
Loading user profile... Error: {error.message} No user data. Name: {user.name} Email: {user.email}
import React, { useState, useEffect } from 'react';
function useDataFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
// ฟังก์ชัน cleanup: ยกเลิก fetch request
return () => {
abortController.abort();
console.log('Data fetch aborted on unmount/re-render');
};
}, [url]); // ดึงข้อมูลใหม่หาก URL เปลี่ยนแปลง
return { data, loading, error };
}
// การใช้งานในคอมโพเนนต์:
function UserProfile({ userId }) {
const { data: user, loading, error } = useDataFetch(`https://api.example.com/users/${userId}`);
if (loading) return User Profile
abortController.abort() ในฟังก์ชัน cleanup มีความสำคัญอย่างยิ่ง หาก UserProfile unmount ในขณะที่ fetch request กำลังดำเนินอยู่ cleanup นี้จะยกเลิก request นั้น ซึ่งจะช่วยป้องกันการใช้ network traffic โดยไม่จำเป็น และที่สำคัญกว่านั้นคือป้องกันไม่ให้ promise resolve ในภายหลังและพยายามเรียก setData หรือ setError ในคอมโพเนนต์ที่ถูก unmount ไปแล้ว
5. การจัดการ DOM และไลบรารีภายนอก
เมื่อคุณโต้ตอบโดยตรงกับ DOM หรือรวมไลบรารีของบุคคลที่สามที่จัดการ DOM elements ของตัวเอง (เช่น ไลบรารี chart, คอมโพเนนต์แผนที่) คุณมักจะต้องดำเนินการตั้งค่าและรื้อถอน
ตัวอย่าง: การเริ่มต้นและทำลายไลบรารี Chart (แนวคิด)
import React, { useEffect, useRef } from 'react';
// สมมติว่า ChartLibrary คือไลบรารีภายนอกเช่น Chart.js หรือ D3
import ChartLibrary from 'chart-library';
function useChart(data, options) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
if (chartRef.current) {
// เริ่มต้นการทำงานของไลบรารี chart เมื่อ mount
chartInstance.current = new ChartLibrary(chartRef.current, { data, options });
}
// ฟังก์ชัน cleanup: ทำลาย instance ของ chart
return () => {
if (chartInstance.current) {
chartInstance.current.destroy(); // สมมติว่าไลบรารีมีเมธอด destroy
chartInstance.current = null;
}
};
}, [data, options]); // เริ่มต้นใหม่หาก data หรือ options เปลี่ยนแปลง
return chartRef;
}
// การใช้งานในคอมโพเนนต์:
function SalesChart({ salesData }) {
const chartContainerRef = useChart(salesData, { type: 'bar' });
return (
chartInstance.current.destroy() ใน cleanup เป็นสิ่งจำเป็นอย่างยิ่ง หากไม่มีสิ่งนี้ ไลบรารี chart อาจทิ้ง DOM elements, event listeners หรือ state ภายในอื่นๆ ไว้เบื้องหลัง ทำให้เกิด memory leak และอาจเกิดความขัดแย้งหากมีการสร้าง chart อื่นในตำแหน่งเดียวกันหรือเมื่อคอมโพเนนต์ถูก re-render
การสร้าง Custom Hooks ที่แข็งแกร่งพร้อมการ Cleanup
พลังของ custom hooks อยู่ที่ความสามารถในการห่อหุ้มตรรกะที่ซับซ้อน ทำให้สามารถนำกลับมาใช้ใหม่และทดสอบได้ การจัดการ cleanup อย่างเหมาะสมภายใน hooks เหล่านี้ช่วยให้แน่ใจว่าตรรกะที่ห่อหุ้มไว้นั้นแข็งแกร่งและปราศจากปัญหาที่เกี่ยวข้องกับ side effects
ปรัชญา: การห่อหุ้มและการนำกลับมาใช้ใหม่
Custom hooks ช่วยให้คุณปฏิบัติตามหลักการ 'Don't Repeat Yourself' (DRY) แทนที่จะกระจายการเรียก useEffect และตรรกะ cleanup ที่สอดคล้องกันไปทั่วหลายคอมโพเนนต์ คุณสามารถรวมศูนย์ไว้ใน custom hook ได้ ซึ่งทำให้โค้ดของคุณสะอาดขึ้น เข้าใจง่ายขึ้น และมีโอกาสเกิดข้อผิดพลาดน้อยลง เมื่อ custom hook จัดการ cleanup ของตัวเอง คอมโพเนนต์ใดๆ ที่ใช้ hook นั้นจะได้รับประโยชน์จากการจัดการทรัพยากรอย่างมีความรับผิดชอบโดยอัตโนมัติ
เรามาปรับปรุงและขยายตัวอย่างก่อนหน้านี้ โดยเน้นการใช้งานในระดับสากลและแนวทางปฏิบัติที่ดีที่สุด
ตัวอย่างที่ 1: useWindowSize – Event Listener Hook ที่ตอบสนองต่อการใช้งานทั่วโลก
Responsive design เป็นกุญแจสำคัญสำหรับผู้ใช้ทั่วโลก เพื่อรองรับขนาดหน้าจอและอุปกรณ์ที่หลากหลาย hook นี้ช่วยติดตามขนาดของหน้าต่าง
Window Width: {width}px Window Height: {height}px
ตอนนี้หน้าจอของคุณ {width < 768 ? 'เล็ก' : 'ใหญ่'}
ความสามารถในการปรับตัวนี้มีความสำคัญอย่างยิ่งสำหรับผู้ใช้บนอุปกรณ์ที่หลากหลายทั่วโลก
import React, { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
// ตรวจสอบว่า window ถูกกำหนดค่าแล้ว สำหรับสภาพแวดล้อม SSR
if (typeof window === 'undefined') {
return;
}
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
// ฟังก์ชัน cleanup: ลบ event listener ออก
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // dependency array ที่ว่างเปล่าหมายความว่าเอฟเฟกต์นี้ทำงานครั้งเดียวเมื่อ mount และ cleanup เมื่อ unmount
return windowSize;
}
// การใช้งาน:
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
dependency array ที่ว่างเปล่า [] ในที่นี้หมายความว่า event listener จะถูกเพิ่มเพียงครั้งเดียวเมื่อคอมโพเนนต์ mount และถูกลบออกเพียงครั้งเดียวเมื่อ unmount ซึ่งป้องกันไม่ให้มีการแนบ listener หลายตัวหรือค้างอยู่หลังจากคอมโพเนนต์หายไป การตรวจสอบ typeof window !== 'undefined' ช่วยให้มั่นใจว่าสามารถเข้ากันได้กับสภาพแวดล้อม Server-Side Rendering (SSR) ซึ่งเป็นแนวปฏิบัติทั่วไปในการพัฒนาเว็บสมัยใหม่เพื่อปรับปรุงเวลาในการโหลดเริ่มต้นและ SEO
ตัวอย่างที่ 2: useOnlineStatus – การจัดการสถานะเครือข่ายทั่วโลก
สำหรับแอปพลิเคชันที่ต้องพึ่งพาการเชื่อมต่อเครือข่าย (เช่น เครื่องมือทำงานร่วมกันแบบเรียลไทม์, แอปซิงโครไนซ์ข้อมูล) การทราบสถานะออนไลน์ของผู้ใช้เป็นสิ่งจำเป็น hook นี้เป็นวิธีในการติดตามสถานะนั้น พร้อมกับการ cleanup ที่เหมาะสม
สถานะเครือข่าย: {isOnline ? 'เชื่อมต่อแล้ว' : 'ไม่ได้เชื่อมต่อ'}
สิ่งนี้สำคัญอย่างยิ่งในการให้ข้อเสนอแนะแก่ผู้ใช้ในพื้นที่ที่มีการเชื่อมต่ออินเทอร์เน็ตไม่เสถียร
import React, { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
useEffect(() => {
// ตรวจสอบว่า navigator ถูกกำหนดค่าแล้ว สำหรับสภาพแวดล้อม SSR
if (typeof navigator === 'undefined') {
return;
}
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// ฟังก์ชัน cleanup: ลบ event listeners ออก
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []); // ทำงานครั้งเดียวเมื่อ mount, cleanup เมื่อ unmount
return isOnline;
}
// การใช้งาน:
function NetworkStatusIndicator() {
const isOnline = useOnlineStatus();
return (
คล้ายกับ useWindowSize, hook นี้เพิ่มและลบ global event listeners ไปยังอ็อบเจกต์ window หากไม่มีการ cleanup, listeners เหล่านี้จะยังคงอยู่ และอัปเดต state ของคอมโพเนนต์ที่ถูก unmount ไปแล้ว ซึ่งนำไปสู่ memory leak และคำเตือนใน console การตรวจสอบ state เริ่มต้นสำหรับ navigator ช่วยให้มั่นใจว่าเข้ากันได้กับ SSR
ตัวอย่างที่ 3: useKeyPress – การจัดการ Event Listener ขั้นสูงเพื่อการเข้าถึง
แอปพลิเคชันแบบโต้ตอบมักต้องการการป้อนข้อมูลจากคีย์บอร์ด hook นี้สาธิตวิธีการฟังการกดปุ่มเฉพาะ ซึ่งมีความสำคัญต่อการเข้าถึง (accessibility) และประสบการณ์ผู้ใช้ที่ดีขึ้นทั่วโลก
กด Spacebar: {isSpacePressed ? 'กดอยู่!' : 'ปล่อยแล้ว'} กด Enter: {isEnterPressed ? 'กดอยู่!' : 'ปล่อยแล้ว'} การนำทางด้วยคีย์บอร์ดเป็นมาตรฐานสากลสำหรับการโต้ตอบที่มีประสิทธิภาพ
import React, { useState, useEffect } from 'react';
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
// ฟังก์ชัน cleanup: ลบ event listeners ทั้งสองออก
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]); // ทำงานซ้ำหาก targetKey เปลี่ยนแปลง
return keyPressed;
}
// การใช้งาน:
function KeyboardListener() {
const isSpacePressed = useKeyPress(' ');
const isEnterPressed = useKeyPress('Enter');
return (
ฟังก์ชัน cleanup ในที่นี้จะลบทั้ง listeners ของ keydown และ keyup อย่างระมัดระวัง เพื่อป้องกันไม่ให้ค้างอยู่ หาก dependency targetKey เปลี่ยนแปลง listeners ก่อนหน้าสำหรับคีย์เก่าจะถูกลบออก และ listeners ใหม่สำหรับคีย์ใหม่จะถูกเพิ่มเข้ามา เพื่อให้แน่ใจว่ามีเพียง listeners ที่เกี่ยวข้องเท่านั้นที่ทำงานอยู่
ตัวอย่างที่ 4: useInterval – Hook การจัดการ Timer ที่แข็งแกร่งด้วย `useRef`
เราได้เห็น useInterval มาก่อนหน้านี้แล้ว ลองมาดูใกล้ๆ ว่า useRef ช่วยป้องกัน stale closures ซึ่งเป็นความท้าทายทั่วไปเกี่ยวกับ timers ใน effects ได้อย่างไร
ตัวจับเวลาที่แม่นยำเป็นพื้นฐานสำหรับแอปพลิเคชันจำนวนมาก ตั้งแต่เกมไปจนถึงแผงควบคุมในอุตสาหกรรม
import React, { useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// จดจำ callback ล่าสุด สิ่งนี้ช่วยให้แน่ใจว่าเรามีฟังก์ชัน 'callback' ที่อัปเดตอยู่เสมอ
// แม้ว่า 'callback' เองจะขึ้นอยู่กับ state ของคอมโพเนนต์ที่เปลี่ยนแปลงบ่อยก็ตาม
// เอฟเฟกต์นี้จะทำงานซ้ำก็ต่อเมื่อ 'callback' เองเปลี่ยนแปลง (เช่น เนื่องจาก 'useCallback')
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// ตั้งค่า interval เอฟเฟกต์นี้จะทำงานซ้ำก็ต่อเมื่อ 'delay' เปลี่ยนแปลง
useEffect(() => {
function tick() {
// ใช้ callback ล่าสุดจาก ref
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]); // ทำงานซ้ำเพื่อตั้งค่า interval ใหม่เมื่อ delay เปลี่ยนแปลงเท่านั้น
}
// การใช้งาน:
function Stopwatch() {
const [seconds, setSeconds] = React.useState(0);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(
() => {
if (isRunning) {
setSeconds((prevSeconds) => prevSeconds + 1);
}
},
isRunning ? 1000 : null // Delay เป็น null เมื่อไม่ได้ทำงาน ซึ่งจะหยุด interval ชั่วคราว
);
return (
Stopwatch: {seconds} seconds
การใช้ useRef สำหรับ savedCallback เป็นรูปแบบที่สำคัญอย่างยิ่ง หากไม่มีสิ่งนี้ หาก callback (เช่น ฟังก์ชันที่เพิ่มค่า counter โดยใช้ setCount(count + 1)) อยู่ใน dependency array ของ useEffect ตัวที่สองโดยตรง interval จะถูกเคลียร์และรีเซ็ตทุกครั้งที่ count เปลี่ยนแปลง ซึ่งจะทำให้ timer ไม่น่าเชื่อถือ โดยการเก็บ callback ล่าสุดไว้ใน ref, interval เองจำเป็นต้องรีเซ็ตก็ต่อเมื่อ delay เปลี่ยนแปลงเท่านั้น ในขณะที่ฟังก์ชัน `tick` จะเรียก `callback` เวอร์ชันล่าสุดเสมอ ซึ่งหลีกเลี่ยง stale closures ได้
ตัวอย่างที่ 5: useDebounce – การเพิ่มประสิทธิภาพด้วย Timers และ Cleanup
Debouncing เป็นเทคนิคทั่วไปในการจำกัดอัตราการเรียกใช้ฟังก์ชัน ซึ่งมักใช้กับช่องค้นหาหรือการคำนวณที่ใช้ทรัพยากรมาก การ Cleanup มีความสำคัญอย่างยิ่งในที่นี้เพื่อป้องกันไม่ให้ timers หลายตัวทำงานพร้อมกัน
Current Search Term: {searchTerm} Debounced Search Term (API call likely uses this): {debouncedSearchTerm} การปรับปรุงการป้อนข้อมูลของผู้ใช้ให้เหมาะสมเป็นสิ่งสำคัญสำหรับการโต้ตอบที่ราบรื่น โดยเฉพาะอย่างยิ่งกับสภาพเครือข่ายที่หลากหลาย
import React, { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// ตั้งค่า timeout เพื่ออัปเดตค่าที่ถูก debounce
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// ฟังก์ชัน cleanup: เคลียร์ timeout หาก value หรือ delay เปลี่ยนแปลงก่อนที่ timeout จะทำงาน
return () => {
clearTimeout(handler);
};
}, [value, delay]); // เรียก effect ซ้ำเมื่อ value หรือ delay เปลี่ยนแปลงเท่านั้น
return debouncedValue;
}
// การใช้งาน:
function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500); // Debounce 500ms
useEffect(() => {
if (debouncedSearchTerm) {
console.log('Searching for:', debouncedSearchTerm);
// ในแอปจริง คุณจะเรียก API ที่นี่
}
}, [debouncedSearchTerm]);
return (
clearTimeout(handler) ใน cleanup ช่วยให้แน่ใจว่าหากผู้ใช้พิมพ์อย่างรวดเร็ว timeouts ที่ค้างอยู่ก่อนหน้าจะถูกยกเลิก มีเพียงอินพุตสุดท้ายภายในช่วงเวลา delay เท่านั้นที่จะเรียก setDebouncedValue ซึ่งจะช่วยป้องกันการทำงานที่ใช้ทรัพยากรมากเกินไป (เช่น การเรียก API) และปรับปรุงการตอบสนองของแอปพลิเคชัน ซึ่งเป็นประโยชน์อย่างยิ่งสำหรับผู้ใช้ทั่วโลก
รูปแบบการ Cleanup ขั้นสูงและข้อควรพิจารณา
แม้ว่าหลักการพื้นฐานของการ cleanup effect จะตรงไปตรงมา แต่แอปพลิเคชันในโลกแห่งความเป็นจริงมักมีความท้าทายที่ซับซ้อนกว่า การทำความเข้าใจรูปแบบและข้อควรพิจารณาขั้นสูงจะช่วยให้ custom hooks ของคุณแข็งแกร่งและปรับเปลี่ยนได้
ทำความเข้าใจ Dependency Array: ดาบสองคม
Dependency array เป็นตัวควบคุมว่าเอฟเฟกต์ของคุณจะทำงานเมื่อใด การจัดการที่ไม่ถูกต้องอาจนำไปสู่ปัญหาสองประการหลัก:
- การละเลย Dependencies: หากคุณลืมใส่ค่าที่ใช้ภายในเอฟเฟกต์ของคุณลงใน dependency array เอฟเฟกต์ของคุณอาจทำงานกับ "stale" closure ซึ่งหมายความว่ามันอ้างอิงถึง state หรือ props เวอร์ชันเก่า ซึ่งอาจนำไปสู่บั๊กที่ละเอียดอ่อนและพฤติกรรมที่ไม่ถูกต้อง เนื่องจากเอฟเฟกต์ (และการ cleanup) อาจทำงานกับข้อมูลที่ล้าสมัย ปลั๊กอิน React ESLint ช่วยตรวจจับปัญหาเหล่านี้ได้
- การระบุ Dependencies มากเกินไป: การใส่ dependencies ที่ไม่จำเป็น โดยเฉพาะอ็อบเจกต์หรือฟังก์ชันที่ถูกสร้างขึ้นใหม่ทุกครั้งที่เรนเดอร์ อาจทำให้เอฟเฟกต์ของคุณทำงานซ้ำ (และดังนั้นจึงต้อง cleanup และตั้งค่าใหม่) บ่อยเกินไป ซึ่งอาจทำให้ประสิทธิภาพลดลง, UI กระพริบ และการจัดการทรัพยากรที่ไม่มีประสิทธิภาพ
เพื่อทำให้ dependencies คงที่ ให้ใช้ useCallback สำหรับฟังก์ชัน และ useMemo สำหรับอ็อบเจกต์หรือค่าที่คำนวณใหม่ได้ยาก hooks เหล่านี้จะจดจำค่าของมัน (memoize) ป้องกันการ re-render ของคอมโพเนนต์ลูกโดยไม่จำเป็น หรือการทำงานซ้ำของเอฟเฟกต์เมื่อ dependencies ของมันไม่ได้เปลี่ยนแปลงอย่างแท้จริง
Count: {count} นี่เป็นการสาธิตการจัดการ dependency อย่างรอบคอบ
import React, { useEffect, useState, useCallback, useMemo } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [filter, setFilter] = useState('');
// Memoize ฟังก์ชันเพื่อป้องกันไม่ให้ useEffect ทำงานซ้ำโดยไม่จำเป็น
const fetchData = useCallback(async () => {
console.log('Fetching data with filter:', filter);
// ลองนึกภาพการเรียก API ที่นี่
return `Data for ${filter} at count ${count}`;
}, [filter, count]); // fetchData จะเปลี่ยนก็ต่อเมื่อ filter หรือ count เปลี่ยน
// Memoize อ็อบเจกต์หากใช้เป็น dependency เพื่อป้องกันการ re-render/effects ที่ไม่จำเป็น
const complexOptions = useMemo(() => ({
retryAttempts: 3,
timeout: 5000
}), []); // dependency array ว่างเปล่าหมายความว่าอ็อบเจกต์ options ถูกสร้างขึ้นครั้งเดียว
useEffect(() => {
let isActive = true;
fetchData().then(data => {
if (isActive) {
console.log('Received:', data);
}
});
return () => {
isActive = false;
console.log('Cleanup for fetch effect.');
};
}, [fetchData, complexOptions]); // ตอนนี้ เอฟเฟกต์นี้จะทำงานก็ต่อเมื่อ fetchData หรือ complexOptions เปลี่ยนแปลงจริงๆ
return (
การจัดการ Stale Closures ด้วย `useRef`
เราได้เห็นแล้วว่า useRef สามารถเก็บค่าที่เปลี่ยนแปลงได้ซึ่งคงอยู่ตลอดการเรนเดอร์โดยไม่ทำให้เกิดการเรนเดอร์ใหม่ ซึ่งมีประโยชน์อย่างยิ่งเมื่อฟังก์ชัน cleanup ของคุณ (หรือตัวเอฟเฟกต์เอง) ต้องการเข้าถึงเวอร์ชัน *ล่าสุด* ของ prop หรือ state แต่คุณไม่ต้องการรวม prop/state นั้นไว้ใน dependency array (ซึ่งจะทำให้เอฟเฟกต์ทำงานซ้ำบ่อยเกินไป)
พิจารณาเอฟเฟกต์ที่บันทึกข้อความหลังจาก 2 วินาที หาก `count` เปลี่ยนแปลง cleanup ต้องการ `count` *ล่าสุด*
Current Count: {count} สังเกตค่า count ใน console หลังจาก 2 วินาทีและเมื่อ cleanup
import React, { useEffect, useState, useRef } from 'react';
function DelayedLogger() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
// อัปเดต ref ให้เป็นค่า count ล่าสุดเสมอ
useEffect(() => {
latestCount.current = count;
}, [count]);
useEffect(() => {
const timeoutId = setTimeout(() => {
// ส่วนนี้จะบันทึกค่า count ที่เป็นปัจจุบันเมื่อ timeout ถูกตั้งค่าเสมอ
console.log(`Effect callback: Count was ${count}`);
// ส่วนนี้จะบันทึกค่า count ล่าสุดเสมอเนื่องจาก useRef
console.log(`Effect callback via ref: Latest count is ${latestCount.current}`);
}, 2000);
return () => {
clearTimeout(timeoutId);
// cleanup นี้จะสามารถเข้าถึง latestCount.current ได้เช่นกัน
console.log(`Cleanup: Latest count when cleaning up was ${latestCount.current}`);
};
}, []); // dependency array ว่างเปล่า เอฟเฟกต์ทำงานครั้งเดียว
return (
เมื่อ DelayedLogger เรนเดอร์ครั้งแรก `useEffect` ที่มี dependency array ว่างเปล่าจะทำงาน `setTimeout` จะถูกตั้งเวลา หากคุณเพิ่มค่า count หลายครั้งก่อนครบ 2 วินาที `latestCount.current` จะถูกอัปเดตผ่าน `useEffect` ตัวแรก (ซึ่งทำงานทุกครั้งที่ `count` เปลี่ยนแปลง) เมื่อ `setTimeout` ทำงานในที่สุด มันจะเข้าถึง `count` จาก closure ของมัน (ซึ่งเป็นค่า count ณ เวลาที่เอฟเฟกต์ทำงาน) แต่จะเข้าถึง `latestCount.current` จาก ref ปัจจุบัน ซึ่งสะท้อน state ล่าสุด ความแตกต่างนี้มีความสำคัญอย่างยิ่งสำหรับเอฟเฟกต์ที่แข็งแกร่ง
Multiple Effects ในคอมโพเนนต์เดียว กับ Custom Hooks
เป็นเรื่องปกติที่สามารถมีการเรียก useEffect หลายครั้งภายในคอมโพเนนต์เดียว อันที่จริง มันเป็นสิ่งที่แนะนำเมื่อแต่ละเอฟเฟกต์จัดการ side effect ที่แตกต่างกัน ตัวอย่างเช่น useEffect หนึ่งอาจจัดการการดึงข้อมูล อีกอันอาจจัดการการเชื่อมต่อ WebSocket และอันที่สามอาจฟัง event ทั่วโลก
อย่างไรก็ตาม เมื่อเอฟเฟกต์ที่แตกต่างกันเหล่านี้มีความซับซ้อน หรือหากคุณพบว่าตัวเองใช้ตรรกะเอฟเฟกต์เดียวกันในหลายคอมโพเนนต์ นั่นเป็นสัญญาณที่ชัดเจนว่าคุณควรแยกตรรกะนั้นออกเป็น custom hook Custom hooks ส่งเสริมการแบ่งส่วน (modularity), การนำกลับมาใช้ใหม่ (reusability) และการทดสอบที่ง่ายขึ้น ทำให้โค้ดเบสของคุณจัดการได้ง่ายและขยายขนาดได้สำหรับโครงการขนาดใหญ่และทีมพัฒนาที่หลากหลาย
การจัดการข้อผิดพลาดใน Effects
Side effects อาจล้มเหลว การเรียก API อาจส่งคืนข้อผิดพลาด การเชื่อมต่อ WebSocket อาจหลุด หรือไลบรารีภายนอกอาจโยน exception custom hooks ของคุณควรจัดการสถานการณ์เหล่านี้อย่างสง่างาม
- การจัดการ State: อัปเดต state ในเครื่อง (เช่น
setError(true)) เพื่อสะท้อนสถานะข้อผิดพลาด ทำให้คอมโพเนนต์ของคุณสามารถเรนเดอร์ข้อความแสดงข้อผิดพลาดหรือ UI สำรองได้ - การบันทึก (Logging): ใช้
console.error()หรือผสานรวมกับบริการบันทึกข้อผิดพลาดทั่วโลกเพื่อจับและรายงานปัญหา ซึ่งมีค่าอย่างยิ่งสำหรับการดีบักในสภาพแวดล้อมและฐานผู้ใช้ที่แตกต่างกัน - กลไกการลองใหม่ (Retry Mechanisms): สำหรับการดำเนินการบนเครือข่าย ลองพิจารณาใช้ตรรกะการลองใหม่ภายใน hook (พร้อม exponential backoff ที่เหมาะสม) เพื่อจัดการกับปัญหาเครือข่ายชั่วคราว ซึ่งช่วยเพิ่มความยืดหยุ่นสำหรับผู้ใช้ในพื้นที่ที่มีการเข้าถึงอินเทอร์เน็ตที่ไม่เสถียร
Loading blog post... (Retries: {retries}) Error: {error.message} {retries < 3 && 'Retrying soon...'} No blog post data. {post.author} {post.content}
import React, { useState, useEffect } from 'react';
function useReliableDataFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [retries, setRetries] = useState(0);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
let timeoutId;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { signal });
if (!response.ok) {
if (response.status === 404) {
throw new Error('Resource not found.');
} else if (response.status >= 500) {
throw new Error('Server error, please try again.');
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
const result = await response.json();
setData(result);
setRetries(0); // รีเซ็ตการลองใหม่เมื่อสำเร็จ
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted intentionally');
} else {
console.error('Fetch error:', err);
setError(err);
// ใช้ตรรกะการลองใหม่สำหรับข้อผิดพลาดบางอย่างหรือจำนวนครั้งที่ลองใหม่
if (retries < 3) { // ลองใหม่สูงสุด 3 ครั้ง
timeoutId = setTimeout(() => {
setRetries(prev => prev + 1);
}, Math.pow(2, retries) * 1000); // Exponential backoff (1s, 2s, 4s)
}
}
} finally {
setLoading(false);
}
};
fetchData();
return () => {
abortController.abort();
clearTimeout(timeoutId); // เคลียร์ timeout การลองใหม่เมื่อ unmount/re-render
};
}, [url, retries]); // ทำงานซ้ำเมื่อ URL เปลี่ยนหรือพยายามลองใหม่
return { data, loading, error, retries };
}
// การใช้งาน:
function BlogPost({ postId }) {
const { data: post, loading, error, retries } = useReliableDataFetch(`https://api.example.com/posts/${postId}`);
if (loading) return {post.title}
hook ที่ปรับปรุงแล้วนี้สาธิตการ cleanup อย่างเข้มข้นโดยการเคลียร์ timeout การลองใหม่ และยังเพิ่มการจัดการข้อผิดพลาดที่แข็งแกร่งและกลไกการลองใหม่ง่ายๆ ทำให้แอปพลิเคชันมีความยืดหยุ่นต่อปัญหาเครือข่ายชั่วคราวหรือข้อผิดพลาดของแบ็กเอนด์ ซึ่งช่วยปรับปรุงประสบการณ์ผู้ใช้ทั่วโลก
การทดสอบ Custom Hooks พร้อม Cleanup
การทดสอบอย่างละเอียดเป็นสิ่งสำคัญอย่างยิ่งสำหรับซอฟต์แวร์ใดๆ โดยเฉพาะอย่างยิ่งสำหรับตรรกะที่นำกลับมาใช้ใหม่ได้ใน custom hooks เมื่อทดสอบ hooks ที่มี side effects และ cleanup คุณต้องแน่ใจว่า:
- เอฟเฟกต์ทำงานอย่างถูกต้องเมื่อ dependencies เปลี่ยนแปลง
- ฟังก์ชัน cleanup ถูกเรียกก่อนที่เอฟเฟกต์จะทำงานซ้ำ (หาก dependencies เปลี่ยนแปลง)
- ฟังก์ชัน cleanup ถูกเรียกเมื่อคอมโพเนนต์ (หรือผู้ใช้ของ hook) unmount
- ทรัพยากรถูกปล่อยอย่างถูกต้อง (เช่น event listeners ถูกลบ, timers ถูกเคลียร์)
ไลบรารีอย่าง @testing-library/react-hooks (หรือ @testing-library/react สำหรับการทดสอบระดับคอมโพเนนต์) มีเครื่องมือช่วยในการทดสอบ hooks แบบแยกส่วน รวมถึงเมธอดในการจำลองการ re-render และ unmount ซึ่งช่วยให้คุณสามารถยืนยันได้ว่าฟังก์ชัน cleanup ทำงานตามที่คาดไว้
แนวทางปฏิบัติที่ดีที่สุดสำหรับ Effect Cleanup ใน Custom Hooks
โดยสรุป นี่คือแนวทางปฏิบัติที่ดีที่สุดที่จำเป็นสำหรับการเชี่ยวชาญการ cleanup effect ใน React custom hooks ของคุณ เพื่อให้แน่ใจว่าแอปพลิเคชันของคุณแข็งแกร่งและมีประสิทธิภาพสำหรับผู้ใช้ทั่วทุกทวีปและอุปกรณ์:
-
จัดให้มี Cleanup เสมอ: หาก
useEffectของคุณลงทะเบียน event listeners, ตั้งค่า subscriptions, เริ่ม timers หรือจัดสรรทรัพยากรภายนอกใดๆ มัน ต้อง return ฟังก์ชัน cleanup เพื่อยกเลิกการกระทำเหล่านั้น -
ทำให้ Effects มีจุดสนใจเดียว: แต่ละ
useEffecthook ควรจัดการ side effect ที่สอดคล้องกันเพียงอย่างเดียว ซึ่งทำให้เอฟเฟกต์อ่านง่าย, ดีบักง่าย และเข้าใจง่าย รวมถึงตรรกะ cleanup ของมันด้วย -
ใส่ใจ Dependency Array ของคุณ: กำหนด dependency array อย่างแม่นยำ ใช้ `[]` สำหรับเอฟเฟกต์ mount/unmount และรวมค่าทั้งหมดจากขอบเขตของคอมโพเนนต์ (props, state, functions) ที่เอฟเฟกต์ต้องใช้ ใช้
useCallbackและuseMemoเพื่อทำให้ฟังก์ชันและอ็อบเจกต์ที่เป็น dependency คงที่ เพื่อป้องกันการทำงานซ้ำของเอฟเฟกต์โดยไม่จำเป็น -
ใช้
useRefสำหรับค่าที่เปลี่ยนแปลงได้: เมื่อเอฟเฟกต์หรือฟังก์ชัน cleanup ของมันต้องการเข้าถึงค่าที่เปลี่ยนแปลงได้ *ล่าสุด* (เช่น state หรือ props) แต่คุณไม่ต้องการให้ค่านั้นกระตุ้นให้เอฟเฟกต์ทำงานซ้ำ ให้เก็บไว้ในuseRefอัปเดต ref ในuseEffectแยกต่างหากโดยมีค่านั้นเป็น dependency - แยกตรรกะที่ซับซ้อน: หากเอฟเฟกต์ (หรือกลุ่มของเอฟเฟกต์ที่เกี่ยวข้อง) มีความซับซ้อนหรือถูกใช้ในหลายที่ ให้แยกมันออกเป็น custom hook ซึ่งช่วยปรับปรุงการจัดระเบียบโค้ด, การนำกลับมาใช้ใหม่ และความสามารถในการทดสอบ
- ทดสอบ Cleanup ของคุณ: รวมการทดสอบตรรกะ cleanup ของ custom hooks ของคุณเข้ากับขั้นตอนการพัฒนาของคุณ ตรวจสอบให้แน่ใจว่าทรัพยากรถูกจัดสรรคืนอย่างถูกต้องเมื่อคอมโพเนนต์ unmount หรือเมื่อ dependencies เปลี่ยนแปลง
-
พิจารณา Server-Side Rendering (SSR): จำไว้ว่า
useEffectและฟังก์ชัน cleanup ของมันจะไม่ทำงานบนเซิร์ฟเวอร์ระหว่าง SSR ตรวจสอบให้แน่ใจว่าโค้ดของคุณจัดการกับการไม่มี API เฉพาะของเบราว์เซอร์ (เช่นwindowหรือdocument) อย่างสง่างามในระหว่างการเรนเดอร์เริ่มต้นบนเซิร์ฟเวอร์ - ใช้การจัดการข้อผิดพลาดที่แข็งแกร่ง: คาดการณ์และจัดการข้อผิดพลาดที่อาจเกิดขึ้นภายในเอฟเฟกต์ของคุณ ใช้ state เพื่อสื่อสารข้อผิดพลาดไปยัง UI และบริการบันทึกข้อมูลเพื่อการวินิจฉัย สำหรับการดำเนินการบนเครือข่าย ให้พิจารณากลไกการลองใหม่เพื่อความยืดหยุ่น
บทสรุป: เสริมพลังแอปพลิเคชัน React ของคุณด้วยการจัดการ Lifecycle อย่างมีความรับผิดชอบ
React custom hooks ควบคู่ไปกับการ cleanup effect อย่างขยันขันแข็ง เป็นเครื่องมือที่ขาดไม่ได้สำหรับการสร้างเว็บแอปพลิเคชันคุณภาพสูง ด้วยการเชี่ยวชาญศิลปะแห่งการจัดการ lifecycle คุณจะป้องกัน memory leak, ขจัดพฤติกรรมที่ไม่คาดคิด, เพิ่มประสิทธิภาพ และสร้างประสบการณ์ที่น่าเชื่อถือและสอดคล้องกันมากขึ้นสำหรับผู้ใช้ของคุณ ไม่ว่าพวกเขาจะอยู่ที่ใด ใช้อุปกรณ์ใด หรือมีสภาพเครือข่ายเป็นอย่างไร
ยอมรับความรับผิดชอบที่มาพร้อมกับพลังของ useEffect ด้วยการออกแบบ custom hooks ของคุณอย่างรอบคอบโดยคำนึงถึงการ cleanup คุณไม่ได้แค่เขียนโค้ดที่ใช้งานได้ แต่คุณกำลังสร้างซอฟต์แวร์ที่ยืดหยุ่น มีประสิทธิภาพ และบำรุงรักษาง่าย ซึ่งทนทานต่อกาลเวลาและการขยายขนาด พร้อมที่จะให้บริการผู้ชมที่หลากหลายและทั่วโลก ความมุ่งมั่นของคุณต่อหลักการเหล่านี้จะนำไปสู่โค้ดเบสที่ดีต่อสุขภาพและผู้ใช้ที่มีความสุขอย่างไม่ต้องสงสัย